跳到主要内容

Java 多线程-Atomic 原子操作类

Atomic 原子操作

JUC 下面提供了一系列原子操作的工具类,它们位于 java.util.concurrent.atomic 包。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

所以,所谓原子类说简单点就是具有原子/原子操作特征的类。

以 AtomicInteger 为例,它提供的主要操作有:

  • 增加值并返回新值:int addAndGet(int delta)
  • 加 1 后返回新值:int incrementAndGet()
  • 获取当前值:int get()
  • 用 CAS 方式设置:int compareAndSet(int expect, int update)

Atomic 类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set

Atomic 原子操作的原理

自己通过 CAS 编写 incrementAndGet(),它大概长这样:

public int incrementAndGet(AtomicInteger var) {
int prev, next;
do {
prev = var.get();
next = prev + 1;
} while ( ! var.compareAndSet(prev, next));
return next;
}

CAS 是指,在这个操作中,如果 AtomicInteger 的当前值是 prev,那么就更新为 next,返回 true。如果 AtomicInteger 的当前值不是 prev,就什么也不干,返回 false。通过 CAS 操作并配合 do ... while 循环,即使其他线程修改了 AtomicInteger 的值,最终的结果也是正确的。

这个 compareAndSet() 方法底层就是调用 Unsafe 的方法

public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);

public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);

public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

Atomic 的常用方法

//获取当前值
int get();

//设置指定值
void set(int newValue)

//设置指定值并返回原来的值
int getAndSet(int newValue)

// i++
int getAndIncrement()

// ++i
int incrementAndGet()

// i--
int getAndDecrement()

// --i
int decrementAndGet()

//当前值加上delta,返回以前的值
int getAndAdd(int delta)

//当前值加上delta,返回新的值
int addAndGet(int delta)

//如果当前值等于入参expect,则把值设为update,并返回ture,如果不等则返回false
boolean compareAndSet(int expect, int update)
// 1.8新增方法,更新当前值,返回以前的值
int getAndUpdate(IntUnaryOperator updateFunction)

// 1.8新增方法,更新当前值,返回更新后的值
int updateAndGet(IntUnaryOperator updateFunction)

// 1.8新增方法,更新当前值,返回以前的值
int getAndAccumulate(int x,IntBinaryOperator accumulatorFunction)

// 1.8新增方法,更新当前值,返回更新后的值
int accumulateAndGet(int x,IntBinaryOperator accumulatorFunction)

演示 AtomicInteger 中 1.8 新增方法的使用方法

/**
* 演示AtomicInteger中1.8新增方法的使用方法
*/
@Test
public void operatorTest(){
AtomicInteger i = new AtomicInteger(0);
//lambda表达式中参数operand表示AtomicInteger的当前值
int andUpdate = i.getAndUpdate(operand -> ++operand);
System.out.println(andUpdate); //result: 0
System.out.println(i.get()); //result: 1

int i1 = i.updateAndGet(operand -> operand-2 );
System.out.println(i1); //result: -1
System.out.println(i.get()); //result: -1

//lambda表达式中参数left表示AtomicInteger的当前值、right表示前面那个参数5
int andAccumulate = i.getAndAccumulate(5, (left, right) -> left + right);
System.out.println(andAccumulate); //result: -1
System.out.println(i.get()); //result: 4

int i2 = i.accumulateAndGet(4, (left, right) -> left + right);
System.out.println(i2); //result: 8
System.out.println(i.get()); //result: 8
}

JUC 包中的原子类是哪 4 类

注意:别忘了了 ABA 问题的存在,适当情况应该使用 AtomicStampedReference

1、基本数据类型

使用原子的方式更新基本类型

  • AtomicInteger:整形原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

2、数组类型

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray:引用类型数组原子类

3、叠加器(没有使用 CAS 并性能高于基本数据类型)

DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder

4、引用类型

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
  • AtomicMarkableReference :原子更新带有标记位的引用类型

5、对象的属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
  • AtomicLongFieldUpdater:原子更新长整形字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

数组类型原子类

数组类型原子类介绍

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray :引用类型数组原子类

上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍。

AtomicIntegerArray 类常用方法

public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

AtomicIntegerArray 常见方法使用

import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicIntegerArrayTest {

public static void main(String[] args) {
// TODO Auto-generated method stub
int temvalue = 0;
int[] nums = { 1, 2, 3, 4, 5, 6 };
AtomicIntegerArray i = new AtomicIntegerArray(nums);
for (int j = 0; j < nums.length; j++) {
System.out.println(i.get(j));
}
temvalue = i.getAndSet(0, 2);
System.out.println("temvalue:" + temvalue + "; i:" + i);
temvalue = i.getAndIncrement(0);
System.out.println("temvalue:" + temvalue + "; i:" + i);
temvalue = i.getAndAdd(0, 5);
System.out.println("temvalue:" + temvalue + "; i:" + i);
}

}

引用类型原子类

引用类型原子类介绍

基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
  • AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

上面三个类提供的方法几乎相同,所以我们这里以 AtomicReference 为例子来介绍。

AtomicReference 类使用示例

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceTest {

public static void main(String[] args) {
AtomicReference<Person> ar = new AtomicReference<Person>();
Person person = new Person("SnailClimb", 22);
ar.set(person);
Person updatePerson = new Person("Daisy", 20);
ar.compareAndSet(person, updatePerson);

System.out.println(ar.get().getName());
System.out.println(ar.get().getAge());
}
}

class Person {
private String name;
private int age;

public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

}

上述代码首先创建了一个 Person 对象,然后把 Person 对象设置进 AtomicReference 对象中,然后调用 compareAndSet 方法,该方法就是通过 CAS 操作设置 ar。如果 ar 的值为 person 的话,则将其设置为 updatePerson。实现原理与 AtomicInteger 类中的 compareAndSet 方法相同。运行上面的代码后的输出结果如下:

Daisy
20

对象的属性修改类型原子类

对象的属性修改类型原子类介绍

如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类。

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
  • AtomicLongFieldUpdater:原子更新长整形字段的更新器
  • AtomicReferenceFieldUpdater :原子更新引用类型里的字段的更新器

要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater() 创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 public volatile 修饰符。

上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerFieldUpdater 为例子来介绍。

AtomicIntegerFieldUpdater 类使用示例

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicIntegerFieldUpdaterTest {
public static void main(String[] args) {
AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

User user = new User("Java", 22);
System.out.println(a.getAndIncrement(user));// 22
System.out.println(a.get(user));// 23
}
}

class User {
private String name;
public volatile int age;

public User(String name, int age) {
super();
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

}

输出结果:

22
23

Reference

参考资料 《Java并发编程的艺术》 参考资料 廖雪峰 使用Atomic 参考资料 2020最新Java并发进阶常见面试题总结